eletrotupi / tcc/ frontend/app/triggers/[id].tsx22ce11e7f70ff2faf4796eef6498cd1ec65bbd31
12.9 KB | 439 lines Raw
1
import { useState, useEffect, use } from "react";
2
import {
3
  StyleSheet,
4
  View,
5
  ScrollView,
6
  Pressable,
7
  ActivityIndicator,
8
} from "react-native";
9
import { Text } from "@/components/ui/Text";
10
import { useLocalSearchParams, router, Stack } from "expo-router";
11
import { format } from "date-fns";
12
import { ptBR } from "date-fns/locale";
13

14
import { Card, SubtleInfoCard } from "@/components/ui/Cards";
15
import { NoLinkMessage } from "@/components/misc/NoLinkMessage";
16
import { Badge } from "@/components/ui/Badge";
17
import { Button } from "@/components/ui/Button";
18
import { Col, Between, Row } from "@/components/ui/LayoutHelpers";
19
import { Section, SectionHeader } from "@/components/ui/Sections";
20
import { Theme, Typography, Colors, Spacing } from "@/constants/theme";
21
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
22
import { getTriggerCategory } from "@/constants/trigger-categories";
23
import { getCareActionMeta } from "@/constants/care-action-categories";
24
import {
25
  useTrigger,
26
  useDeleteTrigger,
27
} from "@/hooks/useTrigger.queries";
28
import { usePatchCareAction } from "@/hooks";
29
import { useCareActionLinkStore } from "@/stores";
30
import { formatMoment } from "@/lib/utils/time";
31
import { getMood } from "@/constants/moods";
32

33
const impactColor = (n: number) => {
34
  if (n <= 1) return '#86efac';
35
  if (n <= 2) return '#22c55e';
36
  if (n <= 3) return '#f59e0b';
37
  if (n <= 4) return '#f97316';
38
  return '#ef4444';
39
};
40

41
export default function TriggerDetailsScreen() {
42
  const { id } = useLocalSearchParams<{ id: string }>();
43
  const { data: trigger, isLoading, isError } = useTrigger(id);
44
  const deleteTrigger = useDeleteTrigger();
45
  const patchCareAction = usePatchCareAction();
46
  const { pendingId, triggerLinked } = useCareActionLinkStore();
47
  const hasPendingCareAction = !!pendingId && !triggerLinked;
48
  const [minutesLeft, setMinutesLeft] = useState(0);
49
  const [canDelete, setCanDelete] = useState(false);
50

51
  useEffect(() => {
52
    const updateTimer = () => {
53
      const createdAt = new Date(trigger?.createdAt);
54
      if (!createdAt) return;
55

56
      const msSinceCreation = Date.now() - createdAt.getTime();
57
      const deletionWindowMs = 5 * 60 * 1000;
58

59
      const newCanDelete = msSinceCreation < deletionWindowMs;
60
      const newMinutesLeft = Math.max(
61
        0,
62
        Math.ceil((deletionWindowMs - msSinceCreation) / 60000),
63
      );
64

65
      setCanDelete(newCanDelete);
66
      setMinutesLeft(newMinutesLeft);
67
    };
68

69
    if (isLoading) return;
70

71
    updateTimer(); // Set initial values
72

73
    const interval = setInterval(updateTimer, 1000);
74

75
    return () => clearInterval(interval);
76
  }, [trigger, isLoading]);
77

78
  if (isLoading) {
79
    return (
80
      <View style={styles.centered}>
81
        <ActivityIndicator size="large" color={Colors.light.tint} />
82
      </View>
83
    );
84
  }
85

86
  if (isError || !trigger) {
87
    return (
88
      <View style={styles.centered}>
89
        <Text style={styles.errorText}>Registro não encontrado.</Text>
90
        <Pressable onPress={() => router.back()} style={styles.backButton}>
91
          <Text style={styles.backButtonText}>Voltar</Text>
92
        </Pressable>
93
      </View>
94
    );
95
  }
96

97
  const meta = getTriggerCategory(trigger.category);
98

99
  const handleDelete = async () => {
100
    await deleteTrigger.mutateAsync(String(trigger.id));
101
    router.back();
102
  };
103

104
  const handleLinkCareAction = async () => {
105
    const careActionId = useCareActionLinkStore.getState().linkTrigger();
106
    if (!careActionId) return;
107
    await patchCareAction.mutateAsync({
108
      id: String(careActionId),
109
      data: { triggerId: trigger.id },
110
    });
111
  };
112

113
  const deleteButtonMsg = () => {
114
    const msg = "Excluir Registro";
115
    const timeLeftStr = `(Disp. por ${minutesLeft < 1 ? "<1" : minutesLeft}:00)`;
116
    const suffix = canDelete ? timeLeftStr : "(Expirado)";
117

118
    return `${msg} ${suffix}`;
119
  };
120

121
  const hasAnyLinks = (
122
    trigger.careActions && trigger.careActions.length > 0
123
  ) || (trigger.moodLinks && trigger.moodLinks.length > 0);
124

125
  return (
126
    <>
127
      <Stack.Screen options={{ title: "Ver detalhes" }} />
128

129
      <ScrollView
130
        style={styles.container}
131
        contentContainerStyle={styles.content}
132
        showsVerticalScrollIndicator={false}
133
      >
134
        {/* Header card */}
135
        <Card style={styles.headerCard}>
136
          <Row style={{ alignItems: "center" }}>
137
            <View style={[styles.iconWrap, { backgroundColor: meta.iconBackground }]}>
138
              <MaterialIcons name={meta.icon} size={22} color={meta.color} />
139
            </View>
140

141
            <Col gap={2}>
142
              <Text style={styles.triggerLabel}>
143
                {meta.label}
144
              </Text>
145
              <Text style={styles.triggerMoment}>
146
                {formatMoment(new Date(trigger.moment))}
147
              </Text>
148
            </Col>
149
          </Row>
150
        </Card>
151

152
        {/* Pending care action link banner */}
153
        {hasPendingCareAction && (
154
          <Pressable
155
            onPress={handleLinkCareAction}
156
            disabled={patchCareAction.isPending}
157
            style={({ pressed }) => [
158
              styles.linkBanner,
159
              pressed && { opacity: 0.75 },
160
            ]}
161
          >
162
            <View style={styles.linkBannerIcon}>
163
              <Ionicons name="link-outline" size={18} color="#1D4ED8" />
164
            </View>
165
            <Text style={styles.linkBannerText}>
166
              Vincular à consulta registrada
167
            </Text>
168
            <Ionicons name="chevron-forward" size={16} color="#1D4ED8" />
169
          </Pressable>
170
        )}
171

172
        {/* Linked moods */}
173
        <Section>
174
          <SectionHeader title="Vínculo" variant="subtle" />
175
          {!!hasAnyLinks ? (
176
            <Col gap={8}>
177
              {trigger.moodLinks.map((link) => {
178
                const moodDef = getMood(link.mood.selectedMood.toLowerCase());
179
                return (
180
                  <Pressable
181
                    key={link.id}
182
                    onPress={() => router.push(`/entry/${link.mood.id}`)}
183
                    style={({ pressed }) => [pressed && { opacity: 0.7 }]}
184
                  >
185
                    <Card style={styles.linkCard}>
186
                      <Row style={{ alignItems: "center", gap: 12 }}>
187
                        <View style={styles.linkIconCircle}>
188
                          <Text style={styles.linkMoodEmoji}>{moodDef?.icon ?? "😐"}</Text>
189
                        </View>
190
                        <Col gap={3} style={{ flex: 1 }}>
191
                          <Row style={{ alignItems: "center", gap: 6 }}>
192
                            <Text style={styles.linkLabel}>
193
                              {moodDef?.label ?? link.mood.selectedMood}
194
                            </Text>
195

196
                            <Badge
197
                              label="Humor"
198
                              variant="yellow"
199
                              style={undefined}
200
                            />
201
                          </Row>
202

203
                          <Text style={styles.linkTime}>
204
                            {formatMoment(new Date(link.mood.moment))}
205
                          </Text>
206
                        </Col>
207
                        <Ionicons name="chevron-forward" size={16} color={Colors.light.disabled} />
208
                      </Row>
209
                      <View style={styles.impactBar}>
210
                        <View
211
                          style={[
212
                            styles.impactFill,
213
                            {
214
                              width: `${(link.perceivedImpact / 5) * 100}%`,
215
                              backgroundColor: impactColor(link.perceivedImpact),
216
                            },
217
                          ]}
218
                        />
219
                      </View>
220
                    </Card>
221
                  </Pressable>
222
                );
223
              })}
224

225
              {(trigger.careActions ?? []).map((ca) => {
226
                const meta = getCareActionMeta(ca);
227
                return (
228
                  <Pressable
229
                    key={ca.id}
230
                    onPress={() => router.push(`/care-actions/${ca.id}`)}
231
                    style={({ pressed }) => [pressed && { opacity: 0.7 }]}
232
                  >
233
                    <Card style={styles.linkCard}>
234
                      <Row style={{ alignItems: 'center', gap: 12 }}>
235
                        <View style={[styles.linkIconCircle, { backgroundColor: meta.iconBackground }]}>
236
                          <MaterialIcons name={meta.icon} size={22} color={meta.color} />
237
                        </View>
238
                        <Col gap={3} style={{ flex: 1 }}>
239
                          <Row style={{ alignItems: 'center', gap: 6 }}>
240
                            <Text style={styles.linkLabel}>{meta.label}</Text>
241
                            <Badge label="Consulta" variant="blue" style={undefined} />
242
                          </Row>
243
                          <Text style={styles.linkTime}>
244
                            {formatMoment(new Date(ca.moment))}
245
                          </Text>
246
                        </Col>
247
                        <Ionicons name="chevron-forward" size={16} color={Colors.light.disabled} />
248
                      </Row>
249
                    </Card>
250
                  </Pressable>
251
                );
252
              })}
253
            </Col>
254
          ) : (
255
            <NoLinkMessage />
256
          )}
257
        </Section>
258

259
        {/* Annotation / notes */}
260
        {trigger.comment && (
261
          <Section>
262
            <SectionHeader title="Anotações" variant="subtle" />
263
            <Card style={{ padding: Spacing.cardGap }}>
264
              <Text style={styles.annotation}>
265
                "{trigger.comment?.trim()}"
266
              </Text>
267
            </Card>
268
          </Section>
269
        )}
270

271
        {/* Delete */}
272
        <View style={styles.section}>
273
          <Text style={styles.sectionLabel}>GESTÃO DO REGISTRO</Text>
274
          <Button
275
            variant="outline"
276
            loading={false}
277
            disabled={!canDelete}
278
            onPress={() => router.push(`/triggers/edit/${trigger.id}`)}
279
            title="Editar Registro"
280
          />
281
          <Button
282
            variant="danger"
283
            loading={deleteTrigger.isPending}
284
            disabled={!canDelete || deleteTrigger.isPending}
285
            onPress={handleDelete}
286
            title={deleteButtonMsg()}
287
          />
288

289
          <SubtleInfoCard
290
            style={styles.deleteInfo}
291
            text={`
292
              A exclusão de registros é permitida apenas nos primeiros 5 minutos
293
              após o envio para garantir a integridade do histórico emocional.
294
              `}
295
          />
296
        </View>
297
      </ScrollView>
298
    </>
299
  );
300
}
301

302
const styles = StyleSheet.create({
303
  container: {
304
    flex: 1,
305
    backgroundColor: Theme.colors.background,
306
  },
307
  content: {
308
    padding: Spacing.containerPadding,
309
    gap: Spacing.sectionGap,
310
    paddingBottom: 48,
311
  },
312
  iconWrap: {
313
    width: 40,
314
    height: 40,
315
    borderRadius: 20,
316
    alignItems: 'center',
317
    justifyContent: 'center',
318
    marginTop: 1
319
  },
320
  headerCardContent: {
321
    flexDirection: "row",
322
    alignItems: "center",
323
    gap: 8,
324
  },
325
  triggerLabel: {
326
    ...Typography.headlineLg,
327
    fontWeight: 700,
328
    color: Colors.light.text,
329
  },
330
  centered: {
331
    flex: 1,
332
    justifyContent: "center",
333
    alignItems: "center",
334
    gap: 12,
335
  },
336
  errorText: {
337
    fontSize: 15,
338
    color: Colors.light.textSecondary,
339
  },
340
  backButton: {
341
    paddingHorizontal: 20,
342
    paddingVertical: 10,
343
    backgroundColor: Theme.colors.tint,
344
    borderRadius: Theme.borderRadius.md,
345
  },
346
  backButtonText: {
347
    fontWeight: "700",
348
    color: Theme.colors.text,
349
  },
350

351
  // Pending link banner
352
  linkBanner: {
353
    flexDirection: 'row',
354
    alignItems: 'center',
355
    gap: 10,
356
    backgroundColor: '#DBEAFE',
357
    borderRadius: 10,
358
    padding: 12,
359
  },
360
  linkBannerIcon: {
361
    width: 32,
362
    height: 32,
363
    borderRadius: 16,
364
    backgroundColor: '#BFDBFE',
365
    alignItems: 'center',
366
    justifyContent: 'center',
367
  },
368
  linkBannerText: {
369
    flex: 1,
370
    fontSize: 14,
371
    fontWeight: '600',
372
    color: '#1D4ED8',
373
  },
374

375
  // Header
376
  headerCard: {
377
    padding: 16,
378
  },
379
  triggerMoment: {
380
    fontSize: 13,
381
    color: Colors.light.textSecondary,
382
  },
383

384
  // Sections
385
  section: {
386
    gap: 8,
387
  },
388
  sectionLabel: {
389
    fontSize: 11,
390
    fontWeight: "600",
391
    letterSpacing: 0.8,
392
    color: Colors.light.textSecondary,
393
    textTransform: "uppercase",
394
  },
395

396
  // Annotation
397
  annotation: {
398
    fontSize: 14,
399
    lineHeight: 22,
400
    color: Colors.light.textSecondary,
401
    fontStyle: "italic",
402
    padding: 4,
403
  },
404
  // Linked mood cards
405
  linkCard: {
406
    padding: Spacing.cardGap,
407
    overflow: "hidden",
408
    gap: 10,
409
  },
410
  linkIconCircle: {
411
    width: 44,
412
    height: 44,
413
    alignItems: "center",
414
    justifyContent: "center",
415
    borderRadius: 20,
416
  },
417
  linkMoodEmoji: {
418
    fontSize: 28,
419
  },
420
  linkTime: {
421
    fontSize: 12,
422
    color: Colors.light.textSecondary,
423
  },
424
  linkLabel: {
425
    fontSize: 15,
426
    fontWeight: "700",
427
    color: Colors.light.text,
428
  },
429
  impactBar: {
430
    height: 3,
431
    backgroundColor: Colors.light.divider,
432
    borderRadius: 99,
433
    overflow: "hidden",
434
  },
435
  impactFill: {
436
    height: "100%",
437
    borderRadius: 99,
438
  },
439
});